<?php
// +-------------------------------------------------------------------------+
// | PHP Version 5                                                           |
// +-------------------------------------------------------------------------+
// | ImaEngine - web development framework                                   |
// | Copyright (c) 2006-2007 Sardar Yumatov <ja.doma@gmail.com>              |
// +-------------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or modify it |
// | under the terms of the GNU Lesser General Public License as published   |
// | by the Free Software Foundation; either version 2.1 of the License, or  |
// | (at your option) any later version.                                     |
// |                                                                         |
// | This library is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of              |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    |
// | See the GNU Lesser General Public License for more details.             |
// |                                                                         |
// | You should have received a copy of the GNU Lesser General Public        |
// | License along with this library; if not, write to the Free Software     |
// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
// +-------------------------------------------------------------------------+

/**
* File contains file system related utils and classes.
*
* Functional from this file is intended to build and validate paths in platform-independent way.
* Also the wrappers over the native IO functions are provided. The main purpose of the wrappers
* to hidde some platfom-dependencies and convert all 'false' return values to <tt>IOException</tt>.
*
* @package common
* @category FileSystem
* @version v1.0
* @author Sardar Yumatov <ja.doma@gmail.com>
* @copyright Copyright &copy; 2006-2007 Sardar Yumatov
*
* @PHP5only
*/

if(!defined('PROCESS')) {
	header('HTTP/1.1 404 Not Found');
	die('NotFound');
}


require_once('lib/GenericExceptions.php');

/**
 * Will be set to true if script is currently running on windows platform.
 * @var boolean
 */
define('FILE_WIN32', defined('OS_WINDOWS') ? OS_WINDOWS : !strncasecmp(PHP_OS, 'win', 3));


/**
* Namespace for all file system related functions.
*
* This library works with only Windows and Unix (POSIX) paths. The path is actually a virtual
* path defined from mergin windows and unix(posix) path rules. This means the directory separator
* my be both '/' and '\' or a system root may be a windows drive on unix. However multi root system
* such as windows can't be mapped to single root systems such as unix or linux without loosing
* root info.
*
* To keep things simple this library simply ignores multiple windows roots. The paths like C:\test
* and D:\test are considered to be equal. If this bechavior is not acceptable, then this class must
* be adjusted to use <tt>Configuration<tt> to map sysem roots.
*
* Warning: unix path's may not contain root directories named as [A-Z]: because this
* is recognized as windows root path (e.g. C:/test). This is prevented directly in the
* code by throwing exception on illegal characters like ':'.
*
* All paths generated by this library will never contain trailing '/' or '\' except if
* explicitly mentioned in documentation. Most of the methods delegate the work to <tt>FilePath</tt>.
* Only is*() methods do working by its own (<tt>FilePath</tt> uses these methods).
*
* <b>Warning</b>: unix user-directory paths (~sardar\test) and windows remote paths (\\computer\path)
* are not supported! This means these paths are simply seen as regular relative
* (~ is allowed entry character) or absolute paths.
*
* @static
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class FileUtils {

	/**
     * Returns true if given path is absolute.
     *
     * Absolute path may not contain '..' and must start from root directory (e.g. / or $:\).
     * Note: this method is fast and based on simple regexp's without much parsing.
     *
     * Warning: empty path and windows drive letter are seen as a root path!
     * Examples:
     *  - '' -- trimmed '/'
     *  - 'C:' -- trimmed 'C:\'
     *
     * Thus if you create <tt>FilePath</tt> from a string recognized as an absolute path, then
     * you will get absolute path as it is handled by <tt>FilePath</tt>.
     *
     * @param string $path path to check
     * @return boolean true if the path is absolute, false otherwise
     */
    public static function isAbsolute($path) {
    	if(!is_string($path)) return false; //not a string is not a path at all
    	if(!strlen($path)) return true; //trimmed unix root path
    	//or is a windows root or not a path, because of :
    	if(strlen($path) == 2 && $path{1} == ':') return (boolean)preg_match('/^[a-zA-Z]:$/', $path);
        if(preg_match('/[\\\\\/]\.\.(?=[\\\\\/]|$)/', $path)) return false; //found /../
        return (boolean)(preg_match('/^(?:[a-zA-Z]:)?[\\\\\/]/', $path));
        //warning: user directories (~) are not absolute, nobody should use these.
    }

    /**
     * Returns true if given string is a valid path.
     * Check is regexp based (single match).
     *
     * Empty string '' is a valid path, representing trimmed absolute unix root-path ('/').
     * Also windows drive letter like 'C:' is also a path, representing absolute windows root-path.
     * Null value is not a valid path, however is accepted by many othe functions (replaced by getcwd()).
     *
     * Warning: if path contains illegal characters as defined by <tt>isValidEntry()</tt>
     * then path will be seen as invalid, while it may be possible on unix (and surely not on Windows).
     *
     * @param string $path string to check
     * @return boolean true if given string is a syntatically valid path, false otherwise
     */
    public static function isPath($path) {
    	//never trim() paths!
    	if(!is_string($path)) return false; //not a string is surely not a path
    	//is a windows root or not a path, because of :
    	if(strlen($path) == 2 && $path{1} == ':') return (boolean)preg_match('/^[a-zA-Z]:$/', $path);
    	//allow '', '/', 'C:/', 'test' and paths
    	return (bool)preg_match('/^(?:(?:[a-zA-Z]:)?[\\\\\/]+)?(?:[^\\\\\/:*?"<>|\']+(?:[\\\\\/]+[^\\\\\/:*?"<>|\']+)*[\\\\\/]*)?$/', $path);
    }

    /**
     * Returns false if path entry contains invalid characters.
     * An entry is an directory or file name. Illegal characters are: <b>\/:*?"<>|'</b>
     * Use this method to ensure $entry is valid file name, not a full path.
     * @param string $entry path entry to check
     * @return boolean true if $entry doesn't contain illegal characters
     */
    public static function isValidEntry($entry) {
		return strpbrk($entry, '\\/:*?"<>|\'') === false;
    }

    /**
     * Returns platform root.
     * This method is usually used when resolving a base path in strict mode. Already
     * absolute path will not throw exceptions if get's resolved against system root.
     *
     * This root is calculated once from getcwd() at first call.
     * Note: do not append DIRECTORY_SEPARATOR to returned string, it is already set!
     *
     * @return string system root path: / on unix, C:\ on windows
     */
    public static function getRoot() {
   		//[xxx]Move to Configuration if multi-root system support is added!
    	static $root = null;
    	if(is_null($root)) {
    		$path = FilePath::createPath(getcwd());
    		if(is_null($root = $path->getRoot())) throw new RuntimeException("getcwd() path is not absolute! I can't work on this buggy platform... or adjust FilePath::getRoot() for this environment!");
    	}
		return $root;
    }

    /**
     * Returns true on unix platform and false on windows platform.
     * Method checks of all paths should be converted to lowercase or not, to match
     * internal organizaition of hosting platform.
     *
     * <b>Warning</b>: if this method returns false, then all paths parsed by this library
     * will be converted to lowercase!
     *
     * @return boolean true if this is unix platform, false if this is windows platform
     */
    public static function isCaseSensitive() {
    	return !FILE_WIN32;
    }

    /**
     * Ensures given $path is a string.
     * This method simply returns $path as is or $path->getPath() if $path is a <tt>FilePath</tt> instance.
     * @throws InvalidArgumentException if $path is not a string nor an object
     * @param string|FilePath $path either string or FilePath instance
     * @return string path as a string for native PHP functions
     */
    public static function getPath($path) {
    	if(!is_string($path) && !is_object($path)) throw new InvalidArgumentException("Given path '{$path}' is not a string nor FilePath instance!");
    	return is_object($path)? $path->getPath(): $path;
    }

    /**
     * Resolve relative $path against $base path.
     * Method simply constructs new <tt>FilePath</tt> from arguments. The $base is expected to be
     * safe and absolute absolute. If not, then $base will be resolved against system-root.
     * Exception will be thrown if base is unsafe. If $strict is set, then exception will be thrown
     * if $base is not the root of $path. If $base is null, then getcwd() will be used. For more
     * detail see <tt>FilePath.createPath()</tt>.
     *
     * <b>Note</b>: the same resolving rules are valid for other methods of this class.
     *
     * @see FilePath.createPath()
     * @throws BasePathException if $strict is set and $base is not the root of $path (security)
     * @param string|FilePath $path original path
     * @param string|FilePath $base base path against which path should be resolved, null for getcwd()
     * @param boolean $strict do not allow $path to point higher than the $base
     * @return string absolute $path as string
     */
    public static function absolutePath($path, $base=null, $strict=false) {
		$fpath = FilePath::createPath($path, true, FilePath::createPath($base, true, self::getRoot(), $strict), $strict); //throws InvalidArgumentException, BasePathException
		return $fpath->getPath();
    }

    /**
     * Check whether $path has equal to $root prefix.
     * Function has meaning only if both paths are absolute or both are relative. Otherwise false
     * will be returned. If $resolve is set, then both path's will be resolved against $base if needed
     * and then check will be performed.
     *
     * @see absolutePath()
     * @see FilePath.isRootOf()
     *
     * @throws BasePathException if $strict is set and $base is not the root of $path or $root (security)
     * @param string|FilePath $root root path
     * @param string|FilePath $path path to check
     * @param boolean $resolve resolve $root and $path to absolute for reliable check
     * @param string|FilePath $base base path against which $path and $root should be resolved, null for getcwd()
     * @param boolean $strict do not allow $path and $root to point higher than the $base
	 * @return boolean true if $root is the base path of $path, otherwise false
     */
    public static function isRootOf($root, $path, $resolve=false, $base=null, $strict=false) {
		$froot = !$resolve? FilePath::createPath($root): FilePath::createPath($root, true, FilePath::createPath($base, true, self::getRoot(), $strict), $strict); //throws InvalidArgumentException, BasePathException
		$fpath = !$resolve? FilePath::createPath($path): FilePath::createPath($path, true, FilePath::createPath($base, true, self::getRoot(), $strict), $strict); //throws InvalidArgumentException, BasePathException
		return $froot->isRootOf($fpath);
    }

	/**
	 * Returns true if two given path's are equal.
	 * This method has meaning only if both paths are absolute or both paths are relative, otherwise
	 * method returns false. It is always a goot idea to enable resolving and provide $base path.
	 *
	 * Paths will be compared without taking in account '.' and '..', they will be resolved. If any
	 * of the paths is unsafe (points higher than own root), then false will be returned.
	 *
	 * <b>Warning</b>: this method compares paths, not the real objects, so path and a link to this path
	 * will be seen as two different things!
	 *
	 * @see absolutePath()
	 * @see FilePath.isEqual()
	 *
	 * @throws BasePathException if $strict is set and $base is not the root of both paths (security)
     * @param string|FilePath $path1 first path to compare
     * @param string|FilePath $path2 second path to compare
     * @param boolean $resolve resolve both paths to absolute for reliable check
     * @param string|FilePath $base base path against which both paths should be resolved, null for getcwd()
     * @param boolean $strict do not allow $path1 and $path2 to point higher than the $base
	 * @return boolean true if $path1 and $path2 are pointing to the same object
	 */
	public static function isEqual($path1, $path2, $resolve=false, $base=null, $strict=false) {
		$fpath1 = !$resolve? FilePath::createPath($path1): FilePath::createPath($path1, true, FilePath::createPath($base, true, self::getRoot(), $strict), $strict); //throws InvalidArgumentException, BasePathException
		$fpath2 = !$resolve? FilePath::createPath($path2): FilePath::createPath($path2, true, FilePath::createPath($base, true, self::getRoot(), $strict), $strict); //throws InvalidArgumentException, BasePathException
		return $fpath1->isEqual($fpath2);
	}

	/**
	 * Returns common path prefix.
	 * Method returns the lowest (longest) possible root directory of given $path1 and $path2.
	 *
	 * @see absolutePath()
	 * @see FilePath.commonRoot()
	 *
	 * @throws BasePathException if $strict is set and $base is not the root of both paths (security)
	 * @param string|FilePath $path1 first path for comparison
	 * @param string|FilePath $path2 second path for comparison
	 * @param boolean $resolve resolve both paths to absolute for reliable check
     * @param string|FilePath $base base path against which both paths should be resolved, null for getcwd()
     * @param boolean $strict do not allow $path1 and $path2 to point higher than the $base
	 * @return string the longest common root of both paths
	 */
	public static function commonRoot($path1, $path2, $resolve=false, $base=null, $strict=false) {
		$fpath1 = !$resolve? FilePath::createPath($path1): FilePath::createPath($path1, true, FilePath::createPath($base, true, self::getRoot(), $strict), $strict); //throws InvalidArgumentException, BasePathException
		$fpath2 = !$resolve? FilePath::createPath($path2): FilePath::createPath($path2, true, FilePath::createPath($base, true, self::getRoot(), $strict), $strict); //throws InvalidArgumentException, BasePathException
		return $fpath1->commonRoot($fpath2);
	}

    /**
     * Returns base directory of given $path.
     * Unlike dirname() this method returns real base directory by resolving al '..'.
     * <b>Note:</b> method returns root ('C:', '' or '.') if this path points to system-root or to '.'
     * @throws UnresolvedException if this path is unsafe
     * @param string|FilePath $path path to process
     * @return string base path
     */
	public static function getBase($path) {
		return FilePath::createPath($path)->getBase();
	}

	/**
     * Returns the name of the entry at which this path points to.
     * Unlik basename() this method resolves all '..' before returning last entry of the path.
     * <b>Note:</b> method returns root ('C:', '' or '.') if this path points to system-root or to '.'
     * @throws UnresolvedException if this path is unsafe
     * @param string|FilePath $path path to process
     * @return string file or directory name pointed by this path
     */
	public static function getFile($path) {
		return FilePath::createPath($path)->getFile();
	}

    /**
     * Returns path to a specific directory.
     * With this method a module can say "a specific directory created at installation time".
     * This method should be usually used by modules to ensure a specific directory
     * really exists or bail out with exception if not.
     *
     * Method resolves $path against $base with strict mode set. This means $path must not
     * point higher than $base and if $path is absolute, then $base must be a system root.
     * Otherwise exception will be thrown.
     *
     * Use <tt>FileUtils::getRoot()</tt> as $base if $path is absolute.
     *
     * @see FileUtils.getRoot()
     * @throws InstallationException if path is not found or points to not a directory
     * @throws BasePathException if $base is not the root of $path (security)
     * @param string|FilePath $path path to directory to check
     * @param string|FilePath $base absolute path against which $path should be resolved, null for getcwd(), FileUtils.getRoot() if $path is already absolute.
     * @return string absolute path to existing directory
     */
    public static function ensureDir($path, $base=null) {
		$fpath = FilePath::createPath($path, true, FilePath::createPath($base, true, self::getRoot(), true), true); //throws InvalidArgumentException, BasePathException
		if(!is_dir($ret = $fpath->getPath()) && !is_readable($ret)) throw new InstallationException("Required directory '{$ret}' doesn't exist or is not readable or is not a directory!");
		return $ret;
    }

    /**
     * Returns path to the file.
     * This method is similar to <tt>ensureDir()</tt> but is expecting a file instead of a directory.
     *
     * This method should be usually used by modules to ensure a specific file
     * really exists or bail out with exception if not.
     * Use <tt>FileUtils::getRoot()</tt> as $base if $path is absolute.
     *
     * @see FileUtils.getRoot()
     * @throws InstallationException if path is not found or points to not a regular file
     * @throws BasePathException if $base is not the root of $path (security)
     * @param string|FilePath $path path to file to check
     * @param string|FilePath $base absolute path against which $path should be resolved, null for getcwd(), FileUtils.getRoot() if $path is already absolute.
     * @return string path to a existing regular file
     */
    public static function ensureFile($path, $base=null) {
		$fpath = FilePath::createPath($path, true, FilePath::createPath($base, true, self::getRoot(), true), true); //throws InvalidArgumentException, BasePathException
		if(!is_file($ret = $fpath->getPath()) || !is_readable($ret)) throw new InstallationException("Required file '{$ret}' doesn't exist or is not readable or is not a regular file!");
		return $ret;
    }

    /**
     * Returns path to directory or a file.
     * This method is similar to <tt>ensureDir()</tt> and <tt>ensureFile()</tt>, but addressed resource may be
     * a file or a directory.
     *
     * This method should be usually used by modules to ensure a specific file or directory
     * really exists or bail out with exception if not.
     * Use <tt>FileUtils::getRoot()</tt> as $base if $path is absolute.
     *
     * @see FileUtils.getRoot()
     * @throws InstallationException if path doesn't exist
     * @throws BasePathException if $base is not the root of $path (security)
     * @param string|FilePath $path path to file or to directory
     * @param string|FilePath $base absolute path against which $path should be resolved, null for getcwd(), FileUtils.getRoot() if $path is already absolute.
     * @return string path to a existing directory or a regular file
     */
    public static function ensureExists($path, $base=null) {
		$fpath = FilePath::createPath($path, true, FilePath::createPath($base, true, self::getRoot(), true), true); //throws InvalidArgumentException, BasePathException
		if(!file_exists($ret = $fpath->getPath()) || !is_readable($ret)) throw new InstallationException("Path '{$ret}' doesn't exist or is not readable!");
		return $ret;
    }

    /**
     * Create new unique file in given directory.
     *
     * Method simply builds the path: $base/$prefix.mt_rand().$suffix
     * until path is created, that doesn't exist. If $touch is set, then
     * file will be directly created.
     *
     * @throws IOException if $touch is set and no enough permissions to create a file
     * @param string|FilePath $base base directory path, where in files should be created
     * @param string $prefix file prefix, may be a path to an existing sub-directory and/or any prefix of the tempfile
     * @param string $suffix file suffix, may not be a path (refer to FileUtils::isValidEntry())
     * @param boolean $touch create file by touch()'ing it
     * @return string path to new unique (non-exising if $touch is not set) file.
     */
    public static function uniqueFile($base, $prefix='tmp_', $suffix='', $touch = false) {
    	if(!FileUtils::isValidEntry($suffix)) throw new InvalidArgumentException("Suffix may not be a subdirectory path!");
    	$fbase = FilePath::createPath($base); //throws InvalidArgumentException
    	if(!is_dir($base = $fbase->getPath()) || !is_readable($base)) throw new InvalidArgumentException("Given base directory does't exists or is not readable: {$base}");
    	$prefix = strtr($prefix, '/\\', DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR);
    	do {
    		$path = $base.DIRECTORY_SEPARATOR.$prefix.mt_rand().$suffix;
    	} while(file_exists($path));
    	if($touch && !touch($path)) throw new IOException("Can't create unique file '{$path}', probably not enough permissions!");
    	return $path;
    }
}



/**
 * Path parser and represenation.
 * The purpose of this class to represent and build paths in platform independent way.
 * The existence of the path is NOT checked, this class only builds the path!
 * Only local files are allowed here, no URI's and no windows \\paths
 *
 * Drive letters of window paths are totaly ignored when comparing two paths or converting to a
 * unix path. Howerver original drive letter will be used if windows path is formated as windows path.
 *
 * Instances of this class are immutable and may be used to combine and compare paths. All created
 * instances will be cached internally, so the same path will never be parsed again. This is actual
 * for base directories.
 *
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
class FilePath {

	/**
	 * All parsed FilePath instances. Since FilePath is immutable we may safely reuse
	 * all parsed data.
	 * @var array
	 */
	private static $parsed = array();

	/**
	 * Curernt platform root directory, either [A-Z]:\ or /
	 * Null if this path is relative
	 * @var string
	 */
	private $root;

	/**
	 * Original (parsed) root for platform specific paths (to not loose windows driver letter)
	 * <p>Note</p>: this is for building the path only! Drive letters are ignored when comparing two paths!
	 * @var string either [A-Z]:\ or /
	 */
	private $real_root;

	/**
	 * Array of path entries, may contain '..'
	 * @var array of string
	 */
	private $entries;

	/**
	 * Resolved from '..' entries or null if unsafe.
	 * @var array or string
	 */
	private $short_entries;

	/**
	 * Constructed path from $entries to speedup <tt>getPath()</tt> operation.
	 * @var string
	 */
	private $path;

	/**
	 * Constructed path from $short_entries to speedup <tt>getPath()</tt> operation. Null if unsafe.
	 * @var string
	 */
	private $short_path;

	/**
	 * Absolute marker. Will be true only if path starts from system-root and doesn't contain any of '..'
	 * @var boolean
	 */
	private $absolute;

	/**
	* True if resolved from '..' path is absolute. This marker is needed becuase some absolute, but not completely
	* resolved paths may be absolute if resolved.
	* @var boolean true if this path is safe and is absolute in resolved form
	*/
	private $short_absolute;

	/**
	 * True on test/../.. and alike.
	 * Thus true if this path points higher than own root.
	 * @var boolean
	 */
	private $unsafe;


	/**
     * Parse given path as <tt>FilePath</tt> instance.
     *
     * Constructor tries to accept common path mistakes like mixing '\' abd '/' or double '//'.
     * This makes parsing more error tolerant.
     *
     * The path will be directly resolved if $resolve is set, this saves additional resolve() call.
     * If $base is empty, then <tt>getcwd()</tt> will be used instead. If $strict is set, then
     * exception will be thrown if this path tries to point higher than the given $base or if
     * this path is already absolute and is not within the given $base.
     *
     * Special values for $path are:
     *   - null -- replaced by getcwd()
     *   - '' -- recognized as system root (trimmed '/')
     *   - 'C:' -- recognized as system root (trimmed 'C:\')
     *
     * Warning: short path of the $base will be used if $resolve is set. If $base is unsafe, then
     * exception will be thrown.
     *
     * @throws InvalidArgumentException if path contains illegal characters (not a path)
     * @throws BasePathException if $strict is set and this path tries to point higher than the $base
     * @throws UnresolvedException if $base is not safe and other cases when $path can't be resolved
     * @param string $path original path to be parsed
     * @param boolean directly resolve path against $base
     * @param FilePath $base base path against which this path should be resolved if needed
     * @param boolean $strict do not allow $path to point higher than the $base
     */
	protected function __construct($path, $resolve=false, $base=null, $strict=false) {
		//handle special cases
		$path = is_null($path)? getcwd(): $path;
		if(strlen($path) == 0) $path='/'; //unix root
		if(strlen($path) == 2 && $path{1} == ':' && preg_match('/^[a-zA-Z]:$/', $path)) $path = $path.'\\'; //windows root
		//calculate root
		$mth=false;
        preg_match('/^((?:[a-zA-Z]\:)?[\/\\\\])?(.*)$/', $path, $mth); //parse root chunks
        if($mth[1]) { //root is specified
        	$this->real_root = strlen($mth[1]) > 1? strtr(strtoupper($mth[1]), '/', '\\'): $mth[1]; // C:\ or /
			if(FILE_WIN32 && strlen($this->real_root) < 3) { //we need to resolve path
				if(false === ($cur = getcwd())) { //this is possible on unix... not sure about windows...
					$this->root = 'C:\\'; //this can't happen... just assume default C which normaly always exists
					trigger_error('Can\'t get current working directory, assuming C:', E_USER_NOTICE); //make whole path readable...
				} else {
					//just because PHP may be buggy
					if(!FileUtils::isAbsolute($cur)) throw new RuntimeException("Returned by getcwd() path on Windows system is not absolute, this is not possible! Bailing out: {$cur}"); //we can't do anything, resolving will lead to endless loop
					$this->root = substr($cur, 0, 3);
				}
			} else $this->root = (!FILE_WIN32)? '/': $this->real_root;
        } else $this->real_root = $this->root = null;
        //ensure als C:\\\ are filtered out, also filter trailing /
        $path = trim((string)$mth[2], '\\/'); //relative path from root, may be empty if only root directory is given

        //inherit properties from base path
        do {
        	if($resolve) {
        		//if we have root, then $base must be root (level == 0)
        		if(!is_null($this->root)) {
        			//we will check isRootOf later in createPath()
        			if($strict && is_null($base->getRoot())) throw new BasePathException("Can't resolve absolute path '{$this->root}{$path}', against relative '{$base->path}'! Strict mode is set!");
        			//else just ignore base
        		} else { //resovle this relative path against base
        			if($base->isUnsafe()) throw new UnresolvedException("Given base path '{$base}' is unsafe! I can't use this for path resolving!");
        			//we are sure short path is available to resolve against
        			$this->unsafe = false;
        			$this->root = $base->root; //null if $base is relative too, thus we stay relative
        			$this->real_root = $base->real_root;
        			$this->absolute = $base->absolute;
	        		$this->short_absolute = $base->short_absolute;
	        		//begin entries
					$dirRealStack = $base->short_entries;
					break;
        		}
        	}

        	//do not inherit begin path
        	$this->unsafe = false;
        	$this->absolute = !is_null($this->root);
        	$this->short_absolute = !is_null($this->root);
        	$dirStack = array();
        	$dirRealStack = array();
        } while(false);

        //tokenize and resolve path
        //things like tst//\/\\/tst are recognized as tst/tst, however createPath() may filter out such paths
        $startlevel = 0; //if strict is enabled, we will use this to not go higher than $base
        for($dir = strtok($path, '/\\'); $dir; $dir = strtok('/\\')) { //strtok - shamest move ever made...
			if($dir != '.') {
                if($dir == '..') { //move back
                	if($resolve && $strict && --$startlevel < 0) throw new BasePathException("Given '{$path}' tries to point higher than '{$base->path}', while strict mode is set!");

                	if(!$this->unsafe && sizeof($dirRealStack) < 1) {
                		if(!is_null($this->root)) throw new UnresolvedException("Given absolute path tries to move higher than root directory! {$this->root}{$path}");
                		if($resolve) throw new UnresolvedException("Can't resolve this path '{$path}' because it appears to be unsafe!");
                		$this->unsafe = true;
                		$this->absolute = $this->short_absolute = false;
                	} elseif(!$this->unsafe) array_pop($dirRealStack);

                	if(!$resolve) {
                		$this->absolute = false; //absolute may not contain ..
                		$dirStack[]='..';
                	}
                } else { //check if characters are allowed
					//if(!FileUtils::isValidEntry($dir)) //this can't happen since we check the whole path at createPath() call
					//	throw new InvalidArgumentException("Path entry '{$dir}' contains illegal characters!");
                	if(!$resolve) $dirStack[] = $dir; //add chunk
                	if(!$this->unsafe) $dirRealStack[]= $dir;
                	if($resolve && $strict) $startlevel++;
                }
            } //else ignore current directory
        }
		//unsafe will be set if $dirRealStack invalidates

		//build path, use $dirRealStack if $resolve is set
        $this->entries = $resolve? $dirRealStack: $dirStack;
        $this->short_entries = $this->unsafe? null: $dirRealStack;
        $this->path = $this->asPlatformPath(FILE_WIN32? 'win': 'posix', false);
        $this->short_path = $this->unsafe? null: $this->asPlatformPath(FILE_WIN32? 'win': 'posix', true);
	}

	/**
	 * Parse given string as a filesystem path.
	 *
	 * Any of the paths may be a string, a <tt>FilePath</tt> instance or null. A string will be parsed to
	 * valid instance. Already parsed <tt>FilePath</tt> will be returned as-is. Null will be replaced by
	 * getcwd() and parsed to valid instance. Read in <tt>__constructor()</tt> documentation.
	 *
	 * Method caches already parsed instances. Once created the valid instance will be returned by subsequent
	 * call. This method doesn't change any of the paths (e.g. no trim() and alike). The path key is used as is.
	 *
	 * @see __constructor()
	 *
	 * @throws InvalidArgumentException if path contains illegal characters (not a path)
     * @throws BasePathException if $strict is set and this path tries to point higher than the $base
     * @throws UnresolvedException if $base is not safe and other cases when $path can't be resolved
     *
     * @param string|FilePath $path original path to be parsed, null for getcwd()
     * @param boolean directly resolve path against $base
     * @param string|FilePath $base base path against which this path should be resolved if needed, null for getcwd()
     * @param boolean $strict do not allow $path to point higher than the $base, but $path may be equal to $base
	 * @return FilePath valid FilePath instance
	 */
	public static function createPath($path, $resolve=false, $base=null, $strict=false) {
		if(is_object($path) && $path instanceof FilePath) {
			if(!$resolve) return $path;
			$bb = FilePath::createPath($base);
			if($path->isAbsolute() && (!$strict || $bb->isRootOf($path) || $path->isEqual($bb))) return $path;
			$path = $path->getPath(); //resolve again
		}
		if(is_null($path)) $path = getcwd();
		if(!is_string($path)) throw new InvalidArgumentException("Path '{$path}' should be a valid string, null or FilePath object!");
		if(!FileUtils::isPath($path)) throw new InvalidArgumentException("Given string '{$path}' is not a valid path!");

		if($resolve) {
			$fbase = self::createPath($base);
			if($fbase->isUnsafe()) throw new BasePathException("Can't resolve path '{$path}' against unsafe base directory '{$base}'!");
		}

		//normalize $path to uniform notation, maybe it is already parsed
		$path = rtrim(str_replace('\\', '/', $path), '/\\');
		if(!isset(self::$parsed[$path]) || ($resolve && !self::$parsed[$path]->isAbsolute())) { //create new non resolved instance
			$pt = $resolve? new FilePath($path, true, $fbase, $strict): new FilePath($path);
			if(!$resolve && $path != '.' && $path != '..') self::$parsed[$path] = $pt;
			//may replace multiple windows roots ;-)
			if(($pp = $pt->getPath()) != '.' && $pp != '..') self::$parsed[$pp] = $pt; //all '.' resolved
			$pp = rtrim(str_replace('\\', '/', $pt->getPath()), '/\\');
			if(($pp = $pt->getPath()) != '.' && $pp != '..') self::$parsed[$pp] = $pt; //separator normalized
		} else $pt = self::$parsed[$path];

		//ensure $strict mode
		if($strict && !$fbase->isRootOf($pt) && !$fbase->isEqual($pt)) {
			$base = FileUtils::getPath($base);
			throw new BasePathException("The path '{$path}' tries to point higher than the '{$base}', or base is relative while given path is not, while strict mode is set!");
		}

		return $pt;
	}

	/**
	 * Returns true only if path starts from root and doesn't contain '..'
	 * If $try_short is set, then all '..' will be resolved. If path is unsafe, then
	 * false will be returned.
	 *
	 * Usually $try_short should be used, but this is not set by default, because similar
	 * argument of <tt>FilePath.getPath()</tt> is not set by default. To avoid misunderstandings.
	 *
	 * @param boolean $try_short if set, then getPath(true) will return absolute path too
	 * @return boolean true if this path is starting from system root and is surely safe
	 */
	public function isAbsolute($try_short=false) {
		return $try_short? $this->short_absolute: $this->absolute;
	}

	/**
	 * Returns true if this path is either absolute or doesn't point higher
	 * than own root (e.g. test/../..).
	 * @return boolean true if this path points higher than own "local root"
	 */
	public function isUnsafe() {
		return $this->unsafe;
	}

	/**
	 * Returns number of path entries in this path.
	 *
	 * Use this method to check how diep the path points from its own local root. Also check if
	 * path is absolute if you want to know the level from system root.
	 *
	 * By default the method counts real directory level, thus all '..' will be resolved.
	 * However if $short is <b>unset</b> then method will simply count all entries in this path,
	 * including '..'. If $short is set and path is unsafe, then method returns -1.
	 *
	 * <b>Note</b> method counts path entries, thus a system-root only path like '/' will return 0.
	 *
	 * @see isUnsafe()
	 * @param boolean $short return the number of all entries instead of the level of this path
	 * @return integer number of all path entries or -1 if path is unsafe
	 */
	public function getLevel($short=true) {
		return $short? ($this->unsafe? -1: sizeof($this->short_entries)): sizeof($this->entries);
	}

	/**
	 * Returns this path as absolute path resolved against $base.
	 * Method simply creates new <tt>FilePath</tt> with given parameters.
	 * @see createPath()
     * @throws BasePathException if $strict is set and $base is not the root of this path
     * @throws UnresolvedException if $base is not safe and other cases when $path can't be resolved
     * @param string|FilePath $base base path against which this path should be resolved if needed, null for getcwd()
     * @param boolean $strict do not allow $path to point higher than the $base
	 * @return FilePath fully resolved absolute path, surely in $base if $strict is set
	 */
	public function absolutePath($base=null, $strict = false) {
		return self::createPath($this->getPath(), true, $base, $strict); //delegate work to createPath()
	}

	/**
     * Returns root directory of this path.
     * This method may preserve original windows drive letter. This is the only diffrence
     * with <tt>FileUtils::getRoot()</tt>.
     * If $original is set, then original path root (as read from the string) will be returned
     * regardless of current platform.
     * Note: do not append DIRECTORY_SEPARATOR to returned string, it is already set!
     * @param boolean $original if set, then original root will be returned.
     * @return string current platform root or null if this path is relative
     */
    public function getRoot($original=false) {
		return $original? $this->real_root: $this->root;
    }


	/**
	 * Returns this path as platform specific path string.
	 * If $short is set, then all '..' will be resolved, however exception
	 * will be thrown if path is unsafe.
	 *
	 * <b>Note:</b> relative path is always prefixed by './'
	 *
	 * @throws UnresolvedException if $short is set and this path is unsafe
	 * @param string $short if set, then all '..' will be resolved
	 * @return string path contained in this object, may be unsafe and relative
	 */
	public function getPath($short=false) {
		if($short && $this->isUnsafe()) throw new UnresolvedException("Can't resolve path '{$this->path}'! Short form is not available becuase path is unsafe!");
		return $short? $this->short_path: $this->path;
	}

	/**
	 * Formath this path as platform specific path.
	 * This method equals to <tt>getPath()</tt> when host platform and requested
	 * platform are equal.
	 *
	 * The $platform string may be any of [win, win32, windows] for windows path
	 * and [unix, linux, posix] for unix path.
	 *
	 * <b>Note:</b> relative path is always prefixed by './'
	 *
	 * @see getPath()
	 * @throws InvalidArgumentException if platform name is not recognized
	 * @throws UnresolvedException if $short is set and this path is unsafe
	 * @param string $platform the platform name
	 * @param string $short if set, then all '..' will be resolved
	 * @return string path contained in this object as platform specific path, may be unsafe and relative
	 */
	public function asPlatformPath($platform, $short=false) {
		static $plat = array('win'=>1, 'win32'=>1, 'windows'=>1, 'unix'=>2, 'posix'=>2, 'linux'=>2);
		if($plat[$platform = strtolower($platform)]) $type = $plat[$platform];
		else throw new InvalidArgumentException("Unrecognized platform '{$platform}'!");
		if($short && $this->isUnsafe()) throw new UnresolvedException("This path '{$this->path}' is unsafe, can't resolve all dots '..'!");

		//determine root
		if(is_null($this->root)) $root = $type == 1? '.\\': './';
		elseif($type == 1 && strlen($root = $this->real_root) < 3 && strlen($root=FileUtils::getRoot()) < 3) $root = 'C:\\'; //we are on unix, this path is unix, but converts to win, use fallback
		elseif($type == 2) $root = '/';

		$path = $root.implode($type == 1? '\\': '/', $short? $this->short_entries: $this->entries);
		return rtrim($path, '/\\');
	}

//====================- Comparison -=================

	/**
	 * Returns true if this path is root of given path.
	 * This method has only meaning if this and given paths are both relative or bot absolute.
	 * Otherwise false will be returned. If two paths are equal, then method will return false!
	 *
	 * Method always returns false if one of the paths is unsafe. All dots '.' and '..' will be
	 * resolved for comparison.
	 *
	 * <b>Warning:</b> this method is case-insensitive on windows platform!
	 *
	 * @param string|FilePath $path child path to check
	 * @return boolean true if this path is the root of given $path, false otherwise
	 */
	public function isRootOf($path) {
		if($this->isUnsafe()) return false;
		$fpath = FilePath::createPath($path);
		if($fpath->isUnsafe() || $fpath->isAbsolute(true) != $this->isAbsolute(true)) return false;
		//if given path is shorter or equal in size, then we can't be a root of it
		if(sizeof($fpath->short_entries) <= sizeof($this->short_entries)) return false;
		//iterate through all short entries
		for($i=0; $i < sizeof($this->short_entries); $i++) {
			if(FILE_WIN32 && strcasecmp($this->short_entries[$i], $fpath->short_entries[$i]) != 0) return false;
			elseif(!FILE_WIN32 && strcmp($this->short_entries[$i], $fpath->short_entries[$i]) != 0) return false;
		}
		return true;
	}

	/**
	 * Returns true if this path equals to given $path.
	 *
	 * This method has only meaning if this and given paths are both relative or bot absolute.
	 * Otherwise false will be returned. Method always returns false if one of the paths is
	 * unsafe. All of '..' will be resolved before comparison.
	 *
	 * <b>Warning</b>: if $path points to a filesystem link, which points to this path the
	 * method will return false anyway, becuase comparison is text based regardless of actual
	 * state of filesystem objects.
	 *
	 * <b>Warning:</b> this method is case-insensitive on windows platform!
	 *
	 * @param string|FilePath $path path to compare
	 * @return boolean true if this path points to exactly the same fs object as given $path.
	 */
	public function isEqual($path) {
		if($this->isUnsafe()) return false;
		$fpath = FilePath::createPath($path);
		if($fpath->isUnsafe() || $fpath->isAbsolute(true) != $this->isAbsolute(true)) return false;
		if(sizeof($fpath->short_entries) != sizeof($this->short_entries)) return false;
		//iterate through all short entries
		for($i=0; $i < sizeof($this->short_entries); $i++) {
			if(FILE_WIN32 && strcasecmp($this->short_entries[$i], $fpath->short_entries[$i]) != 0) return false;
			elseif(!FILE_WIN32 && strcmp($this->short_entries[$i], $fpath->short_entries[$i]) != 0) return false;
		}
		return true;
	}


//===================- slicing -=====================
	/**
     * Returns base directory of this path.
     * Unlike dirname() this method returns real base directory by resolving al '..'.
     * Method simply calls <tt>subRootToLevel(-1, true)</tt>.
     * <b>Note:</b> method returns root ('C:', '' or '.') if this path points to system-root or to '.'
     *
     * @see subRootToLevel()
     * @throws UnresolvedException if this path is unsafe
     * @return string base path, may be relative
     */
    public function getBase() {
		return $this->subRootToLevel(-1, true);
    }

    /**
     * Returns the name of the entry at which this path points to.
     * Unlik basename() this method resolves all '..' before returning last entry of the path.
     * Method simply calls <tt>subFileToLevel(0, true)</tt>.
     * <b>Note:</b> method returns root ('C:', '' or '.') if this path points to system-root or to '.'
     *
     * @see subFileToLevel()
     * @throws UnresolvedException if this path is unsafe
     * @return string file or directory name pointed by this path
     */
    public function getFile() {
		return $this->subFileToLevel(0, true);
    }

    /**
     * Returns path entry by index.
     * The path is a list of separated entries including system-root or dot '.'. The 0 index
     * points to the root of the path, thus returns 'C:', '' or '.'. Next entry from the root
     * has index 1 and so on.
     *
     * If $short is set, then resolved from '..' path will be used, which is always equal or
     * shorter than original path. If $short is set and path is unsafe, then exception will
     * be thrown.
     *
     * @throws UnresolvedException if $short is set and this path is not safe
     * @throws InvalidArgumentException if $index < 0 || $index > entries_number
     * @param integer $index path entry index, starting from 0
     * @return string the path entry
     */
    public function getEntry($index, $short=false) {
    	if($short && $this->isUnsafe()) throw new UnresolvedException("Can't obtain short representation, this path '{$this->path}' is unsafe!");
    	$entr = $short? $this->short_entries: $this->entries;
		if($index < 0 || $index > sizeof($entr)) throw new InvalidArgumentException("Given index '{$index}' must be 0 < idx <= getLevel().");
		if($index == 0) return is_null($this->root)? '.': rtrim($this->root, '\\/');
		return $entr[$index-1];
    }

    /**
     * Returns sub-path of this path from root to specific $level.
     *
     * If $short is not set, then method works with path entries as-is, thus resulting path may contain '..'.
     * Otherwise this method works with resolved entries. Exception will be thrown if $short is set ant
     * this path is unsafe.
     *
     * If $level is negative, then path will be sliced from root to entries_number - $level.
     * For example subRootToLevel(-1) is equivalent to <tt>getBase()</tt>.
     * The $level == 0, then the root directory will be returned ('C:', '' or '.'), becuase it is
     * always the first path entry.
     *
     * @throws UnresolvedException if $short is set and this path is unsafe
     * @throws InvalidArgumentException if ($level > entries_number || $level < -entries_number)
     * @param integer $level the level until which the path must be sliced, if < 0 then (entries_number - $level) will be used
     * @param boolean $short if set, then all '..' will be resolved before slicing
     * @return string path from root to specific level
     */
    public function subRootToLevel($level, $short = false) {
    	if($short && $this->isUnsafe()) throw new UnresolvedException("Can't calculate base path of this directory, the path '{$this->path}' is unsafe!");
    	$entr = $short? $this->short_entries: $this->entries;
		$level = $level < 0? sizeof($entr) + $level: $level;
    	if($level < -1 || $level > sizeof($entr)) throw new InvalidArgumentException("Invalid level: '{$level}' (after normalization).");
		if(is_null($this->root)) $root = '.'.DIRECTORY_SEPARATOR;
		else $root = $this->root;
    	$path = $root.implode(DIRECTORY_SEPARATOR, array_slice($entr, 0, max(0, $level))); //-1 if this is the root
    	return rtrim($path, '/\\');
    }

    /**
     * Returns sub-path of this path from last path entry to specific $level.
     *
     * This method is opposite to <tt>subRootToLevel()</tt>. If $level is negative,
     * then slicing will start from the root, otherwise from last entry. For example
     * subFileToLevel(0) is equivalent to <tt>getFile()</tt>, while
     * subFileToLevel(-1) returns the whole path as getPat().
     *
     * @throws UnresolvedException if $short is set and this path is unsafe
     * @throws InvalidArgumentException if ($level > entries_number || $level < -entries_number)
     * @param integer $level the level until which the path must be sliced, if < 0 then (entries_number - $level) will be used
     * @return string path from last entry to specific level
     */
    public function subFileToLevel($level, $short=false) {
		if($short && $this->isUnsafe()) throw new UnresolvedException("Can't calculate base path of this directory, the path '{$this->path}' is unsafe!");
    	$entr = $short? $this->short_entries: $this->entries;
		$level = $level < 0? -($level + 1): sizeof($entr) - $level;
    	if($level < 0 || $level > sizeof($entr)) throw new InvalidArgumentException("Invalid level: '{$level}' (after normalization).");
    	//$level == 0 include root
		$root = $level == 0? (is_null($this->root)? '.'.DIRECTORY_SEPARATOR: $this->root): '';
    	$path = $root.implode(DIRECTORY_SEPARATOR, array_slice($entr, max(0, $level-1), max(0, sizeof($entr) - $level + 1))); //-1 if this is the root
    	return rtrim($path, '/\\');
    }

    /**
	 * Returns common base prefix for this and given path.
	 *
	 * This method has meaning only when this and given path are both absolute or both relative.
	 * Method woks with resolved path entries, thus no of '..' will be in resulting path.
	 * If both paths are absolute, then <tt>$this->getRoot()</tt> will be used in resulting path.
	 * The path will never contain trailing '/', thus on unix platform method may return empty string '', representing common system-root '/'
	 * If resulting path is relative, then it will be prefixed by './'
	 * If one of the paths is unsafe or they are not equal in absolute state, then null will be returned.
	 *
	 * <b>Warning:</b> this method is case-insensitive on windows platform!
	 *
	 * @param string|FilePath $path path to compare
	 * @return string common base path of this and given paths
	 */
	public function commonRoot($path) {
		if($this->isUnsafe()) return null;
		$fpath = FilePath::createPath($path);
		if($fpath->isUnsafe() || $this->isAbsolute(true) != $fpath->isAbsolute(true)) return null;

		//determine root, always of this instance
		if(is_null($this->root)) $root = '.'.DIRECTORY_SEPARATOR;
		else $root = $this->root;

		//iterate through all short entries
		$ret = array();
		for($i=0; $i < sizeof($this->short_entries); $i++) {
			if(FILE_WIN32 && strcasecmp($this->short_entries[$i], $fpath->short_entries[$i]) != 0) break;
			elseif(!FILE_WIN32 && strcmp($this->short_entries[$i], $fpath->short_entries[$i]) != 0) break;
			$ret[] = $this->short_entries[$i];
		}

		$path = $root.implode(DIRECTORY_SEPARATOR, $ret);
		return rtrim($path, '/\\');
	}

	/**
	 * Returns <tt>FileHandler</tt> instance of this path.
	 * @return FileHandler
	 */
	public function getHandler() {
		return new FileHandler($this);
	}

	/**
	 * Format this object as a string.
	 * Method delegates work to <tt>getPath()</tt>.
	 * @return string this path as a string
	 */
	public function __toString() {
		return $this->getPath();
	}
}

/**
* Simple wrapper over native file IO functions.
* The purpose of this wrapper to throw exceptions on errors. Warnings will not be suppressed.
* All is*() methods directly maps to PHP's native functions, while al "do something" methods
* implements some additional functionalitiy or are better portable.
*
* This class can work only with files and directories. No special handling of sockets, posix devices and alike is
* implemented. However you may retrieve the original path to the resource and perform needed actions by using native
* PHP functions. This class is <b>not</b> intended to replace PHP's native function, rather it attempts
* to fix some brain-damaged bechavior of these functions.
*
* <b>Warning:</b> this class doesn't clear state cache when files gets created or removed! The caller
* must clear state cache by calling <tt>clearstatecache()</tt> if needed.
*
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class FileHandler {

	/**
	 * The path where we point to, alwats absolute.
	 * @var string
	 */
	private $path;

	/**
	 * Create new handler for given $path.
	 * @param string|FilePath $path path to wrap
	 */
	public function __construct($path) {
		$this->path = FileUtils::getPath(FilePath::createPath($path, true));
	}

	/**
	 * Returns path wrapped by this handler.
	 * @return string
	 */
	public function getPath() {
		return $this->path;
	}

	/**
	 * Returns true if this path exists.
	 * @return boolean
	 */
	public function exists() {
		//for reliability do not suppres warning
		return file_exists($this->path);
	}

	/**
	 * Returns true if this path exists and is a directory
	 * @return boolean
	 */
	public function isDirectory() {
		//for reliability do not suppres warning
		return is_dir($this->path);
	}

	/**
	 * Returns true if this path exists and is a regular file.
	 * @return boolean
	 */
	public function isFile() {
		//for reliability do not suppres warning
		return is_file($this->path);
	}

	/**
	 * Returns true if this path exists and is a symbolic link.
	 * @return boolean
	 */
	public function isLink() {
		//for reliability do not suppres warning
		return is_link($this->path);
	}

	/**
	 * Returns true if this process has read permissions to this path.
	 * @return boolean
	 */
	public function isReadable() {
		return is_readable($this->path);
	}

	/**
	 * Returns true if this process has write permissions to this path.
	 * @return boolean
	 */
	public function isWritable() {
		return is_writable($this->path);
	}

	/**
	 * Returns true if this path points to an executable.
	 * @return boolean
	 */
	public function isExecutable() {
		return is_executable($this->path);
	}

	/**
	 * Returns size of the file (wrapper over filesize)
	 * The method throw exception on failure, for example if path points to directory.
	 * The method returns -1 if this path doesn't exists.
	 * @throws IOException on any IO error
	 * @return integer file size in bytes or null if pointed file doesn't exist
	 */
	public function getFileSize() {
		if(!$this->exists()) return null;
		if(false === ($ret = filesize($this->path))) throw new IOException("Can't obtain file size of the path '{$this->path}'");
		return $ret;
	}

	/**
	 * Returns unix time-stamp of the last file modification.
	 * This method uses <tt>filemtime()</tt>. For inode modification time use <tt>getLastMetaChangeTime()</tt>.
	 * @see getLastMetaChangeTime()
	 * @throws IOException on any IO error
	 * @return integer last modification time or null if pointed file doesn't exist
	 */
	public function getLastModifedTime() {
		if(!$this->exists()) return null;
		if(false === ($ret = filemtime($this->path))) throw new IOException("Can't obtain last modification time of the path '{$this->path}'");
		return $ret;
	}

	/**
	 * Returns unix time-stamp of the last inode modification.
	 * Inode will be changed when metadata linked with the file data is changed. Think about changing owner or permissions.
	 * This method uses <tt>filectime()</tt>. For file modification time see <tt>getLastModifedTime()</tt>
	 * @see getLastModifedTime()
	 * @throws IOException on any IO error
	 * @return integer last inode modification time or null if pointed file doesn't exist
	 */
	public function getLastMetaChangeTime() {
		if(!$this->exists()) return null;
		if(false === ($ret = filectime($this->path))) throw new IOException("Can't obtain last inode change time of the path '{$this->path}'");
		return $ret;
	}

	/**
	 * Create file or directory.
	 * If $directory is set, then directory will be created instead of a regular file. The full path
	 * of sub-directories will be created if needed. If $baseonly is set, then only the the path of
	 * sub-directories will be created.
	 * Method throws exception if this path already exists and is not of the correct type (file or directory).
	 * @throws IOException on any IO error
	 * @param boolean $directory create directory instead of a regular file
	 * @param boolean $baseonly if set, then only the base path will be created
	 * @return boolean false if this path already exists and is of correct type, otherwise true
	 */
	public function create($directory=false, $baseonly=false) {
		if($this->exists()) {
			if($this->isDirectory() != $directory) throw new IOException("Path '{$this->path}' already exsts and is of wrong type, ".($directory? 'directory': 'regular file').'is expected!');
			return false;
		}
		$fpath = FilePath::createPath($this->path);
		$level = $fpath->getLevel(true);
		for($i = 0; $i < $level; $i++) {
			if(!file_exists($path = $fpath->subRootToLevel($i, true))) {
				if(!mkdir($path)) throw new IOException("Can't create destination subdirectry '{$path}'!");
			} elseif(!is_dir($path)) throw new IOException("Sub-directory path '{$path}' points to a regular file!");
		}
		if(!$baseonly) {
			if($directory && !mkdir($this->path)) throw new IOException("Can't create directry '{$this->path}'!");
			elseif(!$directory && !touch($this->path)) throw new IOException("Can't create regular file '{$this->path}'!");
		}
		return true;
	}

	/**
	 * Copies this file or directory to $destination.
	 *
	 * The method throws exception if this path doesn't exists. If path points to a file, then this file
	 * will be copied to $destination file, overwriting if it already exists. Native <tt>copy()</tt> is used.
	 *
	 * If path points to a directory, then the contents of the directory will be copied to $destination drectory.
	 * The $destination directory must exist or exception will be thrown. The subdirectories will be created if needed.
	 * All files from the source directory will be copied to destination directory, existing files will be overwritten.
	 *
	 * The method does nothing if this this path equals to $destination path, however notice will be triggered.
	 *
	 * @throws IOException on any IO error
	 * @param string|FilePath $destination path to the destination file
	 * @return boolen false if $destination path equals to this path, otherwise true
	 */
	public function copy($destination) {
		if(!$this->exists()) throw new IOException("This path '{$this->path}' doesn't exist!");
		$destination = FileUtils::getPath(FilePath::createPath($destination, true));
		if(FileUtils::isEqual($this->path, $destination, true)) {
			trigger_error("Given path '{$destination}' is equal to this path. Aborting copy operation.", E_USER_NOTICE);
			return false;
		}
		if(file_exists($destination) && ($this->isDirectory() != is_dir($destination))) throw new IOException("Destination '{$destination}' exists, but this_path.isDirectory() != destination.isDirectory()");
		if($this->isFile()) {
			if(FILE_WIN32 && file_exists($destination)) { //may return false on empty files
				if(false === ($size = filesize($destination))) throw new IOException("Can't obtain file size of given destination file '{$destination}'");
			} else $size = 0;
			if(!copy($this->path, $destination) && (!FILE_WIN32 || $size > 0)) throw new IOException("Can't copy this path '{$this->path}' do destination path '{$destination}'");
		} elseif(!file_exists($destination)) throw new IOException("Destination directory '{$destination}' doesn't exist!");
		else { //copy directory
			if(FileUtils::isRootOf($this->path, $destination)) throw new IOException("Destination directory '{$destination}' is in the source '{$this->path}' directory!");
			$dirs = array();
			$dir = $this->path;
			$destdir = $destination;
			do {
				if(false === ($scan = scandir($dir))) throw new IOException("Can't scan directory '{$dir}'!");
				foreach($scan as $entry) {
					if($entry == '.' || $entry == '..') continue;
					$destpath = $destdir.DIRECTORY_SEPARATOR.$entry;
					$path = $dir.DIRECTORY_SEPARATOR.$entry;
					if(file_exists($destpath) && is_dir($destpath) != is_dir($path)) throw new IOException("Destination path '{$destpath}'.isDirectory() != '{$path}'.isDirectory().");
					if(is_dir($path)) {
						if(!file_exists($destpath)) { //create destination dir
							if(false === ($mode = fileperms($path))) throw new IOException("Can't obtain permissions of the source subdirectory '{$path}'!");
							if(!mkdir($destpath, $mode)) throw new IOException("Can't create destination subdirectry '{$destpath}'!");
						}
						$dirs[] = $path;
						$dirs[] = $destpath;
					} else {
						if(FILE_WIN32) { //may return false on empty files
							if(false === ($size = filesize($destination))) throw new IOException("Can't obtain file size of given destination file '{$destination}'");
						} else $size = 0;
						if(!copy($path, $destpath) && (!FILE_WIN32 || $size > 0)) throw new IOException("Can't copy this path '{$this->path}' do destination path '{$destination}'");
					}
				}

				$destdir = (sizeof($dirs) > 0)? array_pop($dirs): null;
				$dir = (sizeof($dirs) > 0)? array_pop($dirs): null;
			} while(!is_null($dir));
		}
		return true;
	}

	/**
	 * Move file or directory to another place.
	 * This method uses <tt>rename()</tt> to move this path to $destination path. However if deistination path doesn't exist,
	 * then all needed directories will be created. If directory should be moved and $destination directory already exists and
	 * $destination is empty, then $destination will be removed to let <tt>rename()</tt> work correctly. In other cases exception
	 * will be thrown.
	 * The method does nothing if this this path equals to $destination path, however notice will be triggered.
	 *
	 * @throws IOException on any IO error
	 * @param string|FilePath $destination path where to this path will be moved
	 * @return FileHandler handler of the $destination path
	 */
	public function move($destination) {
		if(!$this->exists()) throw new IOException("This path '{$this->path}' doesn't exist!");
		$fdest = FilePath::createPath($destination, true);
		$destination = FileUtils::getPath($fdest);
		if(FileUtils::isEqual($this->path, $fdest, true)) {
			trigger_error("Given path '{$destination}' is equal to this path. Aborting move operation.", E_USER_NOTICE);
			return $this;
		}
		if(file_exists($destination) && $this->isDirectory() != is_dir($destination)) throw new IOException("Destination path '{$destination}'.isDirectory() != this path.isDirectory()");
		if(!is_dir($fdest->getBase())) {
			$level = $fdest->getLevel(true);
			for($i = 0; $i < $level; $i++) {
				if(!file_exists($path = $fdest->subRootToLevel($i, true))) {
					if(!mkdir($path)) throw new IOException("Can't create destination subdirectry '{$path}'!");
				} elseif(!is_dir($path)) throw new IOException("Sub-directory path '{$path}' points to a regular file!");
			}
		}
		if(!rename($this->path, $destination)) throw new IOException("Can't move this path '{$this->path}' do destination '{$destination}'");
		return new FileHandler($destination);
	}

	/**
	 * Delete this file or directory.
	 * If this path points to an existing directory, then all contents of this directory will be
	 * deleted! Be carefull with calling this function. Method does nothing if this path doesn't exist.
	 * <b>Note:</b> this method doesn't clears PHP native cache! Call <tt>clearstatecache()</tt> if you want to check existence again.
	 * @throws IOException on any IO error
	 * @return boolean false if this path didn't exist, otherwise true
	 */
	public function delete() {
		if(!$this->exists()) return false;
		elseif($this->isFile() && !unlink($this->path)) throw new IOException("Can't remove regular file '{$this->path}'!");
		elseif($this->isDirectory()) {
			$dirs = array();
			$dir = $this->path;
			do {
				$empty = true;
				if(false === ($scan = scandir($dir))) throw new IOException("Can't scan directory '{$dir}'!");
				foreach($scan as $entry) {
					if($entry == '.' || $entry == '..') continue;
					if(is_dir($path = $dir.DIRECTORY_SEPARATOR.$entry)) {
						if($empty) $dirs[] = $dir; //set back for repeat
						$dirs[] = $path;
						$empty = false;
					}
					elseif(!unlink($path)) throw new IOException("Can't remove regular file '{$path}'!");
				}
				if($empty && !rmdir($dir)) throw new IOException("Can't remove directory '{$dir}'");
				$dir = sizeof($dirs) > 0? array_pop($dirs): null;
			} while(!is_null($dir));
		}
		return true;
	}

	/**
	 * List all files and directories in this directory.
	 * The method uses <tt>scandir()</tt> to scan the contents of the directory effectively.
	 * If $filter or $type are specified, then dir/readdir sequence is used instead.
	 *
	 * If $filter is set, then each directory entry will be <tt>preg_match()</tt>'ed.
	 * By specifying $type the method will process only entries of specific type:
	 *  'dir' or 'directory' -- only <tt>is_dir()</tt>
	 *  'file' or 'files' -- only <tt>is_file()</tt>
	 *  null -- don't filter at all
	 *
	 * If $sort is set, then <tt>sort()</tt> or <tt>rsort()</tt> is used with default comparison mode.
	 * However notice the <tt>scandir()</tt> may sort elements in another way...
	 *
	 * @throws IOException on any IO error
	 * @param string $sort null - no sorting, 'ascending' or 'asc', 'descending' or 'desc'
	 * @param string $filter - regexp for preg_match, allows only specific entries
	 * @param string $type - list 'directories' or 'dir', 'files' for file, null - list everything
	 */
	public function scan($sort=null, $filter=null, $type=null) {
		static $sorttype = array('ascending'=>0, 'asc'=>0, 'a'=>0, 'descending'=>1, 'desc'=>1, 'd'=>1);
		static $types = array('d'=>0, 'dir'=>0, 'dirs'=>0, 'directory'=>0, 'directories'=>0, 'f'=>1, 'file'=>1, 'files'=>1);
		if(!$this->exists()) throw new IOException("This path '{$this->path}' doesn't exist!");
		elseif(!$this->isDirectory()) throw new IOException("This path '{$this->path}' is not a directory!");
		if(!is_null($sort)) {
			if(!isset($sorttype[$sort = strtolower($sort)])) throw new InvalidArgumentException("Unknown sort type '{$sort}'! Use null, 'asc' or 'desc'!");
			$sort = $sorttype[$sort];
		}
		if(!is_null($type)) {
			if(!isset($types[$type = strtolower($type)])) throw new InvalidArgumentException("Unknown entry type '{$type}'! Use null, 'dir' or 'file'!");
			$type = $types[$type];
		}

		if(is_null($filter) && is_null($type)) { //use native scandir
			if(false === ($res = scandir($this->path, $sort))) throw new IOException("Can't scan directory '{$this->path}' usin native scandir()!");
			return $res;
		}
		//iterate throgh dir/read
		$ret = array();
		if(false === ($dir = opendir($this->path))) throw new IOException("Can't open directoy iterator for path '{$this->path}'! opendir() operation failed!");
		while (false !== ($entry = readdir($dir))) {
			if(!is_null($type)) {
				if($type == 0 && !is_dir($this->path.DIRECTORY_SEPARATOR.$entry)) continue;
				elseif($type == 1 && !is_file($this->path.DIRECTORY_SEPARATOR.$entry)) continue;
			}
			if(!is_null($filter) && !preg_match($filter, $entry)) continue;
			$ret[] = $entry;
		}
		closedir($dir);
		if(!is_null($sort)) {
			if($sort == 0 && !sort($ret)) throw new IOException("The sorting using native sort() is failed. Directory: '{$this->path}'");
			elseif($sort == 1 && !rsort($ret)) throw new IOException("The sorting using native rsort() is failed. Directory: '{$this->path}'");
		}
		return $ret;
	}

	/**
	 * Read the contents of the file to a string.
	 * This method uses <tt>file_get_contents()</tt> to read the file. By default the method reads
	 * entire file, however $offset and $maxlength parameters may be used to limit read data.
	 * Method reads until EOF if $maxlength is longer than the length of the file.
	 * @throws IOException on any IO error
	 * @param integer $offset where to start from, should be >=0
	 * @param integer $maxlength maximum length of the data to read, null to read until EOF
	 * @return string read data from $offset to $maxlength or end of file whichever comes first
	 */
	public function getFileContents($offset=0, $maxlength=null) {
		if(!$this->exists() || $this->isDirectory() || !$this->isReadable()) throw new IOException("This path '{$this->path}' doesn't exist or is not readable or is a directory!");
		if(!is_integer($offset) || $offset < 0) throw new InvalidArgumentException("Offset argument ({$offset}) must be integer >= 0!");
		//fucking PHP will return empty string on $maxlength == null, while default is null...
		$maxlength = is_null($maxlength)? $this->getFileSize(): $maxlength;
		if(!is_integer($maxlength) || $maxlength < 0) throw new InvalidArgumentException("Maxlength argument ({$maxlength}) must be integer >= 0 or null!");
		if(false === ($res = file_get_contents($this->path, false, null, $offset, $maxlength))) throw new IOException("Can't read entire file '{$this->path}'!");
		return $res;
	}

	/**
	 * Read contents of this file to the output buffer.
	 * This method uses <tt>readfile()</tt> if whole file should be read. Otherwise new <tt>FileStream</tt>
	 * is used to output content, which uses other optimizations.
	 * @throws IOException on any IO error
	 * @param integer $offset where to start from, should be >=0
	 * @param integer $maxlength maximum length of the data to read, null for unbounded
	 * @return integer number of bytes read from the file
	 */
	public function readFileContents($offset=0, $maxlength=null) {
		if(!$this->exists() || $this->isDirectory() || !$this->isReadable()) throw new IOException("This path '{$this->path}' doesn't exist or is not readable or is a directory!");
		if(!is_integer($offset) || $offset < 0) throw new InvalidArgumentException("Offset argument ({$offset}) must be integer >= 0!");
		if(!is_null($maxlength) && (!is_integer($maxlength) || $maxlength < 0)) throw new InvalidArgumentException("Maxlength argument ({$maxlength}) must be integer >= 0 or null!");
		if($offset == 0 && is_null($maxlength)) {
			if(false === ($res = readfile($this->path))) throw new IOException("Readfile (reading whole file) operation failed!");
		} else {
			$stream = $this->openStream('r');
			$stream->seek($offset);
			$res = $stream->output($maxlength);
			$stream->close();
		}
		return $res;
	}


	/**
	 * Read string lines from the file to array.
	 * This method uses <tt>file()</tt> to read the contents of the file.
	 * @throws IOException on any IO error
	 * @return array of string with line end characters
	 */
	public function getLines() {
		if(!$this->exists() || $this->isDirectory() || !$this->isReadable()) throw new IOException("This path '{$this->path}' doesn't exist or is not readable or is a directory!");
		if(false === ($res = file($this->path))) throw new IOException("Fail to read file contents '{$this->path}' using native file()!");
		return $res;
	}

	/**
	 * Write $data to a file pointed by this path.
	 * This method uses <tt>file_put_contents()</tt> to perform the job, this means the $data may be
	 * a string, array or a stream. If $append is set, then data will be appended to the end of the file,
	 * otherwise the contents of the file will be overwritten.
	 * Method always acquires exclusieve lock to the destination path.
	 *
	 * @throws IOException on any IO error
	 * @param mixed $data anything that file_put_contents() may handle
	 * @param boolean $append enable append mode
	 * @param resource $context the PHP's native stream context if $data is a PHP's native stream
	 * @return integer amount of bytes were written to the file
	 */
	public function putFileContents($data, $append=false, $context=null) {
		if($this->exists() && (!$this->isWritable() || $this->isDirectory())) throw new IOException("This path '{$this->path}' points to an existing directory or is not writable!");
		$this->create(); //ensure we exists
		if(false === ($res = file_put_contents($this->path, $data, LOCK_EX | ($append? FILE_APPEND: 0), $context))) throw new IOException("Failed to write contents to the file '{$this->path}'!");
		return $res;
	}

	/**
	 * Returns new <tt>FileStream</tt> in given $mode and at given $offset.
	 * Method simply <tt>fopen()</tt>'s this path. The stream will always be in 'binary' mode (no line-end conversions).
	 *
	 * Possible $mode's are defined by <tt>fopen()</tt> method, the string $mode will be additionally parsed anyway.
	 * The regexp <tt>(([rwx]\+?)|a)b?</tt> defines all possible modes. The 't' flag is not allowed. The 'b' (binary) flag will
	 * be set by default. Each mode specifies additional restrictions, for example 'x' mode doesn't allow the file to
	 * do already exist.
	 *
	 * The mode 'a+' is not allowed since it is bogus, bechaves differently depending on current sequence of read and write
	 * operations. If you need read capability, then open stream as 'r+' and move pointer to the end of the stream.
	 *
	 * Opened stream will always acquire locking depending on the $mode. For only read streams the shared locking is used.
	 * For writable streams the exclusieve locking is always acquired. If $dont_block is set, then the thread will not block
	 * until lock is acquired, rather it throws exception. This is usefull when a thread tries to write to a specific file,
	 * but will quickly write to another file if first one is used by other thread.
	 *
	 * @see FileStream
	 * @throws IOException on any IO error
	 * @throws UnavailableException if $dont_block is set and file is locked by other thread
	 * @param string $mode mode as described by <tt>fopen()</tt>
	 * @param boolean $dont_block disable waiting for the resource (thread blocking)
	 * @return FileStream new file stream
	 */
	public function openStream($mode, $dont_block=false) {
		if(!$this->exists() || $this->isDirectory()) throw new IOException("This path '{$this->path}' doesn't exist or is a directory!");
		if(!preg_match('/^(?:(?:[rwx]\\+?)|a)b?$/', ($mode = strtolower(trim($mode))))) throw new InvalidArgumentException("Bogus stream mode '{$mode}'! Only (([rwx]\\+?)|a)b? are allowed!");
		if(strpbrk($mode, 'b') === false) $mode.='b';
		$ext = strpbrk($mode, '+') !== false;
		$read = $write = false;

		if($mode{0} == 'r') {
			if(!$this->exists()) throw new IOException("Can't open path '{$this->path}' in read mode '{$mode}' (r) because it doesn't exist!");
			if(!$this->isReadable() || ($ext && !$this->isWritable())) throw new IOException("Not enough permissions to file '{$this->path}' for mode '{$mode}'!");
			$read = true; $write = $ext;
		} else {
			if($this->exists()) {
				if($mode{0} == 'x') throw new IOException("Can't open path '{$this->path}' in write mode '{$mode}' (x) because it exists!");
				if(!$this->isWritable() || ($ext && !$this->isReadable())) throw new IOException("Not enough permissions to file '{$this->path}' for mode '{$mode}'!");
			} else $this->create(false, true); //ensure all directories are created
			$write = true; $read = $ext;
		}
		if(false === ($res = fopen($this->path, $mode))) throw new IOException("Can't open path '{$this->path}' in mode '{$mode}'!");
		//ensure locking
		$wbl = null;
		if(!flock($res, ($write? LOCK_EX: LOCK_SH) | ($dont_block? LOCK_NB: 0), $wbl)) {
			if($wbl) throw new UnresolvedException("Can't acquire proper blocking for path '{$this->path}' in ".($write? 'exclusieve mode': 'shared mode'));
			throw new IOException("Can't lock the file '{$this->path}' in".($write? 'exclusieve mode': 'shared mode'));
		}
		return new FileStream($this, $res, $mode, $write, $read);
	}
}

/**
* Wrapper over <tt>fopen()</tt> resource.
* This class has nothing to do with PHP's native stream support.
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class FileStream {

	/** The handler that has instantiated this stream.
	 * @var FileHandler */
	private $handler;

	/** The native PHP resource from <tt>fopen()</tt>
	 * @var resource */
	private $resource;

	/** The mode string as was used by fopen() call.
	 * @var string */
	private $mode;

	/** The read(1)/write(2)/seek(4) capabilities as binary mask
	 * @var integer */
	private $acc_mode;

	/** Open flag. True if stream is still open.
	 * @var boolean */
	private $isopen;

	/**
	 * Create new stream instance.
	 * <b>Do not use this constructor! It is public because PHP doesn't support package friendly access.</tt>
	 * @param FileHandler $hndl the file handler that has created this stream
	 * @param resource $resource the resource as returned by <tt>fopen()</tt>
	 * @param string $mode the mode as defined by <tt>fopen()</tt> and normalized by file handler
	 */
	public function __construct(FileHandler $hndl, $resource, $mode, $write, $read) {
		$this->handler = $hndl;
		$this->resource = $resource;
		$this->mode = $mode;
		$this->acc_mode = ($read? 1: 0) + ($write? 2: 0) + ($mode{0} == 'a'? 0: 4);
		$this->isopen = true;
	}

	/**
	 * Returns handler that has instantiated this stream.
	 * @return FileHandler
	 */
	public function getHandler() {
		return $this->handler;
	}

	/**
	 * Returns native PHP resource as returned by <tt>fopen()</tt>.
	 * Resource may not be closed, because the close state is stored in instance of this class (there is no other way).
	 * Stream position may be freely changes, thus read and write operations are allowed. Changing lock may lead to
	 * race-conditions, don't do that.
	 * @return retsource
	 */
	public function getResource() {
		return $this->resource;
	}

	/**
	 * Returns mode as was used by <tt>fopen()</tt>
	 * @return string
	 */
	public function getMode() {
		return $this->mode;
	}

	/**
	 * Returns true if read operations are allowed on this stream.
	 * @return boolean
	 */
	public function canRead() {
		if(!$this->isopen) return false;
		return (boolean)($this->acc_mode & 1);
	}

	/**
	 * Returns true if write operations are allowed on this stream.
	 * @return boolean
	 */
	public function canWrite() {
		if(!$this->isopen) return false;
		return (boolean)($this->acc_mode & 2);
	}

	/**
	 * Returns true if this stream supports <tt>fseek()</tt> operation.
	 * Streams opened with 'a' (append) mode are not seekable.
	 *
	 * If this method returns false, then seek(), getPosition() and other
	 * position related methods will throw exception.
	 *
	 * @return boolean
	 */
	public function canSeek() {
		if(!$this->isopen) return false;
		return (boolean)($this->acc_mode & 4);
	}

	/**
	 * Returns current locking.
	 * The locking is always acquired depending on the capabilities of the stream.
	 * This method is for info only
	 * @throws IOException if this stream is closed
	 * @return string either 'shared' or 'exclusieve'
	 */
	public function getLock() {
		if(!$this->isopen) return null;
		return $this->canWrite()? 'exclusieve': 'shared';
	}

	/**
	 * Returns true if <tt>close()</tt> was not called.
	 * This method doesn't check if resource is still available and valid, because PHP doesn't
	 * support such functionality.
	 * @return boolean true if this stream is still open
	 */
	public function isOpen() {
		return $this->isopen;
	}

	/**
	 * Close the stream.
	 * All subsequent operations will throw exception after this method call.
	 * The method may throw exception if operation fails, but internal 'close' will be set anyway.
	 * PHP allows to 'forget' about proper stream closing, but be sure the lock held by the resource
	 * will not be released until <tt>close()</tt> is called or thread dies!
	 * Method does nothing if this stream is already closed.
	 * @throws IOException if operation fails
	 * @return boolean false if this stream was already closed
	 */
	public function close() {
		if(!$this->isopen) return false;
		$this->isopen = false;
		if(!fclose($this->resource)) throw new IOException('Failed to close stream properly!');
		return true;
	}

	/**
	 * Returns current position in the stream in bytes.
	 * Only seekable streams support this operation.
	 * @see canSeek()
	 * @throws IOException if this stream is not seekable or is closed
	 * @return integer the byte offset from the beginning of the stream
	 */
	public function getPosition() {
		if(!$this->isopen) throw new IOException('Stream is closed');
		elseif(!$this->canSeek()) throw new IOException('This stream is not seekable, the pointer is always at the end of stream (append mode)');
		if(false === ($res = ftell($this->resource))) throw new IOException('Can\'t obtain stream pointer position!');
		return $res;
	}

	/**
	 * Set the stream position to a new offset.
	 * Only seekable streams support thi opertion. The $whence argument defines
	 * the way offset should be calculated. Read more in <tt>fseek()</tt>
	 * documentation.
	 *
	 * Note that you may set offset at any position in the stream without restrictions. If new
	 * position is beyond the EOF, then the space between the EOF and pointer will be filled by \0.
	 * Each seek operation resets <tt>isAtEndOfStream()</tt> status to <b>false</b> regardless of the
	 * new position.
	 *
	 * Only 'a' (append) mode streams are not seekable, the pointer is always at end of the file.
	 * @see canSeek()
	 * @throws IOException if this stream is not seekable or is closed
	 * @param integer $offset offset in bytes, may be negative for SEEK_END $whence
	 * @param integer $whence the anchor type for the offset, one of SEEK_SET (default), SEEK_CUR or SEEK_END
	 * @return void
	 */
	public function seek($offset, $whence=null) {
		if(!$this->isopen) throw new IOException('Stream is closed');
		elseif(!$this->canSeek()) throw new IOException('This stream is not seekable, the pointer is always at the end of stream (append mode)');
		$whence = is_null($whence)? SEEK_SET: $whence;
		if($whence != SEEK_SET && $whence != SEEK_END && $whence != SEEK_CUR) throw new InvalidArgumentException("Given argument whence '{$whence}' is not recognized!");
		if(fseek($this->resource, $offset, $whence) < 0) throw new IOException('Can\'t change stream pointer position! Operation seek failed!');
	}

	/**
	 * Set the current position the be beginning of the stream.
	 * This method equals to <tt>seek(0)</tt>
	 * @see canSeek()
	 * @throws IOException if this stream is not seekable or is closed
	 * @return void
	 */
	public function rewind() {
		if(!$this->isopen) throw new IOException('Stream is closed');
		elseif(!$this->canSeek()) throw new IOException('This stream is not seekable, the pointer is always at the end of stream (append mode)');
		if(!rewind($this->resource)) throw new IOException('Can\'t change stream pointer position! Operation rewind failed!');
	}

	/**
	 * Returns true if subsequent read will return empty string because the stream is at end.
	 *
	 * This status will be reset to <b>false</b> with each seek or write operation. Only the
	 * read operation can chage the EOF status. It is possible to move the pointer far beyond
	 * the EOF, this status will be reset anyway. With first read the empty string will be
	 * returned then and this status will be changed to <b>true</b>.
	 * @return boolean true if subsequent read call will return empty string because there is no more data in the stream
	 */
	public function isAtEndOfStream() {
		if(!$this->isopen) return true;
		return feof($this->resource);
	}

	/**
	 * Read up to $length bytes from this stream at current position.
	 * Method returns null if stream is at EOF. This bechavior is more usual to PHP instead of throwing
	 * exceptions.
	 * @throws IOException if this stream is closed or is not readable
	 * @param integer $length
	 * @return string the read data
	 */
	public function read($length) {
		if(!$this->isopen) throw new IOException('Stream is closed');
		elseif(!$this->canRead()) throw new IOException("Stream is not readable!");
		elseif($this->isAtEndOfStream()) return null;
		if(!is_integer($length) || $length < 0) throw new InvalidArgumentException("Length argument ({$length}) must be integer >= 0!");
		if(false === ($res = fread($this->resource, $length))) throw new IOException("Can't read {$length} bytes from the stream!");
		return $res;
	}

	/**
	 * Read data until the new line character (\r\n) or up to $maxlength whichever comes first.
	 * Method uses <tt>fgetc()</tt> which has no limitations since PHP4.3
	 * Method returns null if stream is at EOF. This bechavior is more usual to PHP instead of throwing
	 * exceptions.
	 * @throws IOException if this stream is closed or is not readable
	 * @param integer $maxlength limit the string to $maxlength bytes, null for unbounded (up to EOF)
	 * @return string the read string including trailing line end character
	 */
	public function readLine($maxlength=null) {
		if(!$this->isopen) throw new IOException('Stream is closed');
		elseif(!$this->canRead()) throw new IOException("Stream is not readable!");
		elseif($this->isAtEndOfStream()) return null;
		if(!is_null($maxlength) && (!is_integer($maxlength) || $maxlength < 0)) throw new InvalidArgumentException("Maxlength argument ({$maxlength}) must be integer >= 0 or null!");
		//PHP doesn't handle $maxlength == null as "use default idiot!", thus dubble call...
		if(!is_null($maxlength) && false === ($res = fgets($this->resource, $maxlength))) throw new IOException("Can't read line from the stream!");
		elseif(is_null($maxlength) && false === ($res = fgets($this->resource))) throw new IOException("Can't read line from the stream!");
		return $res;
	}

	/**
	 * Reads up to $length bytes from the file at current position and output it to the output buffer.
	 * This function is more efficient when data should be send directly to the output.
	 * The file will be read in blocks of $blocksize (4kb by default).
	 *
	 * If $length is null, then read will not stop until end of stream (EOF). To speedup this special
	 * case the <tt>fpassthru()</tt> function is used. The $blocksize is ignored then.
	 *
	 * @throws IOException if this stream is closed or is not readable
	 * @param integer $length limit data to $length bytes, null to read until EOF
	 * @param integer $blocksize the read buffer size, null for default 4kb
	 * @return integer number of bytes send to output, either until $length or EOF
	 */
	public function output($length=null, $blocksize=null) {
		if(!$this->isopen) throw new IOException('Stream is closed');
		elseif(!$this->canRead()) throw new IOException("Stream is not readable!");
		elseif($this->isAtEndOfStream()) return 0;
		if(is_null($length)) {
			if(false === ($ret = fpassthru($this->resource))) throw new IOException("Can't output data from this stream!");
			return $ret;
		}
		if(!is_integer($length) || $length < 0) throw new InvalidArgumentException("Length argument ({$length}) must be integer >= 0 or null!");
		$blocksize = is_null($blocksize)? 4096: intval($blocksize);
		if(!is_integer($blocksize) || $blocksize < 512) throw new InvalidArgumentException("Blocksize ({$blocksize}) must be at least 512 or null!");

		for($i=0; !feof($this->resource) && $i < $length; ) {
			$len = min($length - $i, $blocksize);
			if(false === ($dat = fread($this->resource, $len))) throw new IOException("Can't read {$len} bytes from the stream!");
			echo $dat;
			$i += strlen($dat);
		}
		return $i;
	}

	/**
	 * Write $data to the stream at current position.
	 *
	 * The method uses <tt>fwrite()</tt> to write the data. This means if <tt>magic_quotes_runtime()</tt>
	 * is set, then you may encounter funny things in your file. The method doesn't fix it for you to let you
	 * think about what you are doing.
	 *
	 * @throws IOException if this stream is closed or is not writable
	 * @param string $data the string data to write
	 * @param integer $offset the offset in $data where from start reading, 0 for beginning of the string
	 * @param integer $length number of characters (bytes) to write from $offset, may point beyond the end of string, only real data will be written
	 * @return integer number of bytes written to the stream, may be less than $length if $offset + $length point beyound the end of string
	 */
	public function write($data, $offset = 0, $length = null) {
		if(!$this->isopen) throw new IOException('Stream is closed');
		elseif(!$this->canWrite()) throw new IOException("Stream is not writable!");
		if(!is_string($data)) throw new InvalidArgumentException("Data to write should be a valid string!");
		if(!is_integer($offset) || $offset < 0) throw new InvalidArgumentException("Offset argument ({$offset}) must be integer >= 0!");
		if(!is_null($length) && (!is_integer($length) || $length < 0)) throw new InvalidArgumentException("Length argument ({$length}) must be integer >= 0 or null!");
		$data = $offset > 0? substr($data, $offset): strval($data);
		$length = is_null($length)? strlen($data): $length; //becuase PHP is to dumb to understand null as default
		if(false === ($ret = fwrite($this->resource, $data, $length))) throw new IOException("Can't write data to the stream!");
		//should we check $ret to be sure in $length?
		return $ret;
	}

	/**
	 * Enforce write of the output buffer to the file.
	 * PHP buffers all write operations for efficiency, this method forces a write of buffers to the file.
	 * Usually you don't need this method because buffers will be flushed when stream is closed.
	 * The method does nothing if this stream is not writable.
	 * @throws IOException if this stream is closed
	 * @return void
	 */
	public function flush() {
		if(!$this->isopen) throw new IOException('Stream is closed');
		elseif(!$this->canWrite()) return;
		if(!fflush($this->resource)) throw new IOException('Flush operation failed!');
	}

	/**
	 * Truncate opened file to given $size.
	 * If current position is beyond new size, then pointer will be paced at the end of truncated file. This is true
	 * even for non seekable streams (only 'a' mode, file pointer is always at end).
	 *
	 * @throws IOException if this stream is closed or is not writable
	 * @param integer $size new size of the file in bytes
	 * @return void
	 */
	public function truncate($size) {
		if(!$this->isopen) throw new IOException('Stream is closed');
		elseif(!$this->canWrite()) throw new IOException("Stream is not writable!");
		if(!is_integer($size) || $size < 0) throw new InvalidArgumentException("Size argument ({$size}) must be integer >= 0!");
		if($this->canSeek() && false === ($pos = ftell($this->resource))) throw new IOException("Can't obtain current stream pointer position!");
		if(!ftruncate($this->resource, $size)) throw new IOException("Can't truncate this file (stream) to size {$size}!");
		if($this->canSeek() && $pos > $size && fseek($this->resource, $size, SEEK_SET) < 0) throw new IOException("Can't change stram pointer position to {$size}!");
	}
}
?>